文档🔗👉 (opens new window)

所谓客户端激活,指的是 Vue 在浏览器端接管由服务端发送的静态 HTML,使其变为由 Vue 管理的动态 DOM 的过程。

由于服务端已经渲染好了 HTML,所以在客户端不需要重新创建全部的 DOM 元素。客户端激活就是激活服务端返回的 HTML,使之成为响应式。

有两种方式标记 HTML 是服务端放返回:

  • <div id="app" data-server-rendered="true" />
  • app.$mount("#app", true)

通过上面方式可获取当前 vnode 是否为服务端渲染,在对服务端和客户端的 vnode 进行 patch 的时候,会进一步判断 oldVnode 是否为真实的 DOM 元素节点,是的话则:

  • 调用 hydrate 对两端数据进行混合。
  • 调用 invokeCreateHooks,激活客户端数据,如:绑定标签的绑定事件等。
  • 调用 invokeInsertHooks,调用组件插入钩子。
function createPatchFunction (backend) {
  // ...
  return function patch(oldVnode, vnode, hydrating, removeOnly) {
    // ...
    var isRealElement = isDef(oldVnode.nodeType);
    if (isRealElement) {
      // 判断是否为服务端渲染
      if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
        oldVnode.removeAttribute(SSR_ATTR);
        hydrating = true;
      }
      if (isTrue(hydrating)) {
        // 服务端渲染客户端激活逻辑
        if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
          invokeInsertHook(vnode, insertedVnodeQueue, true);
          return oldVnode
        } else {
          warn(
            'The client-side rendered virtual DOM tree is not matching ' +
            'server-rendered content. This is likely caused by incorrect ' +
            'HTML markup, for example nesting block-level elements inside ' +
            '<p>, or missing <tbody>. Bailing hydration and performing ' +
            'full client-side render.'
          );
        }
      }
    }
  }
}

# hydrate 函数

function hydrate(elm, vnode, insertedVnodeQueue, inVPre) {
  var i;
  var tag = vnode.tag;
  var data = vnode.data;
  var children = vnode.children;
  inVPre = inVPre || (data && data.pre);
  vnode.elm = elm;

  // 判断是否为注释或者异步组件
  if (isTrue(vnode.isComment) && isDef(vnode.asyncFactory)) {
    vnode.isAsyncPlaceholder = true;
    return true
  }
  // 检验 node 节点是否匹配
  {
    if (!assertNodeMatch(elm, vnode, inVPre)) {
      return false
    }
  }
  if (isDef(data)) {
    // 如果是子组件,则初始化子组件
    if (isDef(i = data.hook) && isDef(i = i.init)) { i(vnode, true /* hydrating */); }
    if (isDef(i = vnode.componentInstance)) {
      // child component. it should have hydrated its own tree.
      initComponent(vnode, insertedVnodeQueue);
      return true
    }
  }
  // 标签节点
  if (isDef(tag)) {
    if (isDef(children)) {
      // empty element, allow client to pick up and populate children
      if (!elm.hasChildNodes()) {
        // 实际节点没有子节点,客户端的节点有子节点,则创建子节点并添加到旧节点中
        createChildren(vnode, children, insertedVnodeQueue);
      } else {
        // v-html and domProps: innerHTML
        if (isDef(i = data) && isDef(i = i.domProps) && isDef(i = i.innerHTML)) {
          // v-html 和 domProps,则判断 innerHTML 是否相同
          if (i !== elm.innerHTML) {
            /* istanbul ignore if */
            if (typeof console !== 'undefined' &&
                !hydrationBailed
               ) {
              hydrationBailed = true;
              console.warn('Parent: ', elm);
              console.warn('server innerHTML: ', i);
              console.warn('client innerHTML: ', elm.innerHTML);
            }
            return false
          }
        } else {
          // 迭代并比较两端子列表
          var childrenMatch = true;
          var childNode = elm.firstChild; // 获取旧节点列表的第一个节点
          for (var i$1 = 0; i$1 < children.length; i$1++) {
            // 遍历新 vnode 列表

            // 如果 childNode 不存在,说明实际节点列表的长度和新 vnode 列表不相等,则混合失败
            // 如果有子节点,则递归调用 hydrate 进行混合
            // 混合失败直接跳出循环
            if (!childNode || !hydrate(childNode, children[i$1], insertedVnodeQueue, inVPre)) {
              childrenMatch = false;
              break
            }
            childNode = childNode.nextSibling; // 获取旧下一个兄弟节点
          }
          // childNode 不为空,说明实际子节点列表比新 vnode 列表多,混合失败
          if (!childrenMatch || childNode) {
            /* istanbul ignore if */
            if (typeof console !== 'undefined' &&
                !hydrationBailed
               ) {
              hydrationBailed = true;
              console.warn('Parent: ', elm);
              console.warn('Mismatching childNodes vs. VNodes: ', elm.childNodes, children);
            }
            return false
          }
        }
      }
    }
    // 获取当前 vnode 的绑定信息,如:事件等
    if (isDef(data)) {
      var fullInvoke = false;
      for (var key in data) {
        if (!isRenderedModule(key)) { // 判断是否属性是否在客户端已初始化
          fullInvoke = true;
          invokeCreateHooks(vnode, insertedVnodeQueue); // 绑定事件等
          break
        }
      }
      if (!fullInvoke && data['class']) {
        // 确保收集深层绑定的 deps 以进行更新
        traverse(data['class']);
      }
    }
  } else if (elm.data !== vnode.text) {
    // 文本节点且实际文本与新 vnode 的文本内容不同,则直接用更新文本内容
    elm.data = vnode.text;
  }
  return true
}

函数执行逻辑为:

  1. 首先判断是否为注释或者异步组件,是的话直接返回 true,说明混合成功。
  2. 接着调用 assertNodeMatch 函数校验实际 DOM 节点与新 vnode 的 node 节点是否匹配,失败的话返回 false,说明混合失败。
  3. 接着判断是否是子组件,如果是子组件,则调用 initComponent 初始化子组件。
  4. 如果是标签节点,客户端的节点(新 vnode)有子节点,但实际节点没有子节点,则创建子节点并添加到旧节点中。
  5. 如果是标签节点,客户端的节点(新 vnode)和实际节点都有子节点:
    1. 如果是 v-html 和 domProps(render 函数中有该属性) 的话,则判断 innerHTML 是否相同,不相同则返回 false;
    2. 迭代并比较两端子列表。遍历逻辑为:遍历新 vnode 列表,获取实际节点列表的每一个节点 childNode。如果childNode 不存在,说明实际节点列表的长度和新 vnode 列表不相等,则混合失败;如果有子节点,则递归调用 hydrate 进行混合。循环遍历结束后,如果 childNode 不为空,说明实际子节点列表比新 vnode 列表多,混合失败。
    3. 在处理完上面两个逻辑之后,标签节点还会通过 vnode.data 获取当前 vnode 的绑定信息,然后通过 isRenderedModule 判断节点属性是否在服务端已经处理过,如果未处理过,如:事件等,则调用 invokeCreateHooks 进行处理。
  6. 如果是文本节点且实际的节点内容与新 vnode 文本内容不相同,则直接用更新文本内容。

# assertNodeMatch 函数

该函数用于校验实际 DOM 节点与新 vnode 的 node 节点是否匹配。

function assertNodeMatch (node, vnode, inVPre) {
  if (isDef(vnode.tag)) {
    return vnode.tag.indexOf('vue-component') === 0 || (
      !isUnknownElement$$1(vnode, inVPre) &&
      vnode.tag.toLowerCase() === (node.tagName && node.tagName.toLowerCase())
    )
  } else {
    return node.nodeType === (vnode.isComment ? 8 : 3)
  }
}

# isRenderedModule Map

用于判断标签属性是否在客户端已经处理过。

var isRenderedModule = makeMap('attrs,class,staticClass,staticStyle,key');

function makeMap (
  str,
  expectsLowerCase
) {
  var map = Object.create(null);
  var list = str.split(',');
  for (var i = 0; i < list.length; i++) {
    map[list[i]] = true;
  }
  return expectsLowerCase
    ? function (val) { return map[val.toLowerCase()]; }
    : function (val) { return map[val]; }
}

# invokeCreateHooks 函数

用于处理未在服务端处理过的标签属性,如:事件绑定等。

function invokeCreateHooks(vnode, insertedVnodeQueue) {
  for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
    cbs.create[i$1](emptyNode, vnode);
  }
  i = vnode.data.hook; // Reuse variable
  if (isDef(i)) {
    if (isDef(i.create)) { i.create(emptyNode, vnode); }
    if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }
  }
}

其中,cbs 在初始化 patch 函数时,会收集所有的事件 hook 到 cbs 中,如下格式:

function createPatchFunction (backend) {
 var i, j;
 var cbs = {};

 var modules = backend.modules;
 var nodeOps = backend.nodeOps;
 
   // 收集所有的事件 hook 到 cbs 中
 for (i = 0; i < hooks.length; ++i) {
   cbs[hooks[i]] = [];
   for (j = 0; j < modules.length; ++j) {
     if (isDef(modules[j][hooks[i]])) {
       cbs[hooks[i]].push(modules[j][hooks[i]]);
     }
   }
 } 
}
// cbs 数据内容
cbs = {
 activate: [ƒ]
 create: (8) [
   updateAttrs(oldVnode, vnode),
   updateClass(oldVnode, vnode),
   updateDOMListeners(oldVnode, vnode),
   updateDOMProps(oldVnode, vnode),
   updateStyle(oldVnode, vnode),
   _enter(_, vnode),
   create(_, vnode),
   updateDirectives(oldVnode, vnode)
 ],
 destroy: (2) [ƒ, ƒ]
 remove: [ƒ]
 update: (7) [ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ]
}

举个例子,如下面的 div 绑定了点击事件 tap,还有 attr title 属性。

<template>
  <div @click="tap" :title="txt">{{ msg }}</div>
</template>

<script>
export default {
  data() {
    return {
      msg: 'hello'
    }
  },
  computed: {
    txt() {
      return 'aaa'
    }
  },
  methods: {
    tap() {
      console.log('>> tap :')
      alert('hello')
    }
  }
}
</script>

编译 template 后的 vnode.data 数据如下:

data = {
  attrs: {
    title: "aaa"
  },
  on: {
    click: ƒ (),
    length: 0
  	name: "bound tap"
  } 
}

通过 isRenderedModule 判读只有 on 未在服务端处理过,则会调用 invokeCreateHooks 函数,进而出发 cb.create.updateDOMListeners 进行 DOM 事件绑定注册。

# invokeInsertHook 函数

在执行完 hydrate 函数逻辑之后,会调用:invokeInsertHook(vnode, insertedVnodeQueue, true)。用于延迟执行插入组件根节点的钩子,直到元素节点被挂在插入后再执行。

function invokeInsertHook(vnode, queue, initial) {
  if (isTrue(initial) && isDef(vnode.parent)) {
    // 异步执行。由于在服务端已经初始化过了,所以走的是延迟执行的逻辑
    vnode.parent.data.pendingInsert = queue;
  } else {
    // 未初始化过,直接执行
    for (var i = 0; i < queue.length; ++i) {
      queue[i].data.hook.insert(queue[i]);
    }
  }
}

// 在组件初始化时,会判断 pendingInsert 是否存在
// 是的话则将异步队列中的数据添加到 insertedVnodeQueue 后面
// 然后调用 invokeCreateHooks 一并执行。
function initComponent (vnode, insertedVnodeQueue) {
  if (isDef(vnode.data.pendingInsert)) {
    insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);
    vnode.data.pendingInsert = null;
  }
  vnode.elm = vnode.componentInstance.$el;
  if (isPatchable(vnode)) {
    invokeCreateHooks(vnode, insertedVnodeQueue);
    setScope(vnode);
  } else {
    // empty component root.
    // skip all element-related modules except for ref (#3455)
    registerRef(vnode);
    // make sure to invoke the insert hook
    insertedVnodeQueue.push(vnode);
  }
}